/*
 * Tranquil Java Integrated Development Environment
 *
 * The GNU General Public License Version 3
 *
 * Copyright (C) 2021 Autumn Lamonte
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
 * @version 1
 */
package tjide.debugger;

import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

import com.sun.jdi.ArrayReference;
import com.sun.jdi.ArrayType;
import com.sun.jdi.BooleanType;
import com.sun.jdi.ByteType;
import com.sun.jdi.CharType;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.ClassType;
import com.sun.jdi.DoubleType;
import com.sun.jdi.Field;
import com.sun.jdi.FloatType;
import com.sun.jdi.IntegerType;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.LongType;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ShortType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.StringReference;
import com.sun.jdi.Type;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.Value;
import com.sun.jdi.VirtualMachine;

/**
 * JavaDebugValue represents a modifiable variable value in a JVM process.
 */
public class JavaDebugValue implements DebugValue {

    /**
     * Translated strings.
     */
    private static ResourceBundle i18n = ResourceBundle.getBundle(JavaDebugValue.class.getName());

    // ------------------------------------------------------------------------
    // Variables --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * A local variable.
     */
    private LocalVariable localVariable;

    /**
     * An object instance or static class field.
     */
    private Field field;

    /**
     * The frame reference.
     */
    private StackFrame frame;

    /**
     * The object reference.
     */
    private ObjectReference objectReference;

    /**
     * The type reference.
     */
    private ReferenceType referenceType;

    /**
     * The index to an array value.
     */
    private int arrayIndex;

    /**
     * The array value.
     */
    private Value arrayValue;

    /**
     * The array reference.
     */
    private ArrayReference arrayReference;

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Package private constructor.
     *
     * @param localVariable a local variable
     * @param frame the stack frame
     */
    JavaDebugValue(final LocalVariable localVariable, final StackFrame frame) {
        assert (localVariable != null);
        assert (frame != null);

        this.localVariable = localVariable;
        this.frame = frame;
    }

    /**
     * Package private constructor.
     *
     * @param field a field
     * @param objectReference the object
     */
    JavaDebugValue(final Field field, final ObjectReference objectReference) {
        assert (field != null);
        assert (objectReference != null);

        this.field = field;
        this.objectReference = objectReference;
    }

    /**
     * Package private constructor.
     *
     * @param field a field
     * @param referenceType the type
     */
    JavaDebugValue(final Field field, final ReferenceType referenceType) {
        assert (field != null);
        assert (referenceType != null);

        this.field = field;
        this.referenceType = referenceType;
    }

    /**
     * Package private constructor.
     *
     * @param arrayIndex the index in the array for this value
     * @param arrayValue the array value at this index
     * @param arrayReference the array
     */
    JavaDebugValue(final int arrayIndex, final Value arrayValue,
        final ArrayReference arrayReference) {

        assert (arrayValue != null);
        assert (arrayReference != null);

        this.arrayIndex = arrayIndex;
        this.arrayValue = arrayValue;
        this.arrayReference = arrayReference;
    }

    // ------------------------------------------------------------------------
    // DebugValue -------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Get the name for this value.
     *
     * @return the name
     */
    public String getName() {
        if (field != null) {
            return field.name();
        }
        if (localVariable != null) {
            return localVariable.name();
        }
        if (arrayValue != null) {
            return Integer.toString(arrayIndex);
        }
        return i18n.getString("unknownName");
    }

    /**
     * Get the type name for this value.
     *
     * @return the type name
     */
    public String getType() {
        String name = null;
        try {
            if (field != null) {
                name = field.type().name();
            }
            if (localVariable != null) {
                name = localVariable.type().name();
            }
            if (arrayValue != null) {
                name = arrayValue.type().name();
            }
        } catch (ClassNotLoadedException e) {
            // Just return the "unknown type" string below.
            name = null;
        }

        if (name == null) {
            return i18n.getString("unknownType");
        }
        if (name.startsWith("java.lang.")) {
            return name.substring(10);
        }
        return name;
    }

    /**
     * Get a human-readable value for this value.
     *
     * @return the value
     */
    public String getValue() {
        Value value = null;
        if (field != null) {
            if (objectReference != null) {
                try {
                    value = objectReference.getValue(field);
                } catch (IllegalArgumentException e) {
                    return i18n.getString("error");
                }
            } else if (referenceType != null) {
                try {
                    value = referenceType.getValue(field);
                } catch (IllegalArgumentException e) {
                    return i18n.getString("error");
                }
            }
        }
        if (localVariable != null) {
            value = frame.getValue(localVariable);
        }
        if (arrayValue != null) {
            value = arrayValue;
        }

        if (value == null) {
            // The value is actually null.
            return "(null)";
        }

        // DEBUG
        /*
        System.err.println("       frame = " + frame);
        System.err.println("       localVariable = " + localVariable);
        System.err.println("       field = " + field);
        System.err.println("       objectReference = " + objectReference);
        System.err.println("       referenceType = " + referenceType);
        System.err.println("       arrayIndex = " + arrayIndex);
        System.err.println("       arrayValue = " + arrayValue);
        System.err.println("       arrayReference = " + arrayReference);
         */

        assert (value != null);

        if (value instanceof StringReference) {
            return ((StringReference) value).value();
        } else {
            return value.toString();
        }
    }

    /**
     * Set a new value for this value.
     *
     * @param value the new value
     */
    public void setValue(final String value) {
        if (value.length() == 0) {
            return;
        }

        Type type = null;
        try {
            if (localVariable != null) {
                type = localVariable.type();
            }
            if (field != null) {
                type = field.type();
            }
            if (arrayValue != null) {
                type = arrayValue.type();
            }
        } catch (ClassNotLoadedException e) {
            // Error, so we cannot modify.
            return;
        }

        Value newValue = null;
        VirtualMachine virtualMachine = type.virtualMachine();

        if (type instanceof BooleanType) {
            if (value.toLowerCase().trim().equals("true")) {
                newValue = virtualMachine.mirrorOf(true);
            } else if (value.toLowerCase().trim().equals("false")) {
                newValue = virtualMachine.mirrorOf(false);
            }
        }

        if (type instanceof ByteType) {
            try {
                newValue = virtualMachine.mirrorOf(Byte.parseByte(
                    value.trim()));
            } catch (NumberFormatException e) {
                // SQUASH
            }
        }

        if (type instanceof CharType) {
            newValue = virtualMachine.mirrorOf(value.charAt(0));
        }

        if (type instanceof DoubleType) {
            try {
                newValue = virtualMachine.mirrorOf(Double.parseDouble(
                    value.trim()));
            } catch (NumberFormatException e) {
                // SQUASH
            }
        }

        if (type instanceof FloatType) {
            try {
                newValue = virtualMachine.mirrorOf(Float.parseFloat(
                    value.trim()));
            } catch (NumberFormatException e) {
                // SQUASH
            }
        }

        if (type instanceof IntegerType) {
            try {
                newValue = virtualMachine.mirrorOf(Integer.parseInt(
                    value.trim()));
            } catch (NumberFormatException e) {
                // SQUASH
            }
        }

        if (type instanceof LongType) {
            try {
                newValue = virtualMachine.mirrorOf(Long.parseLong(
                    value.trim()));
            } catch (NumberFormatException e) {
                // SQUASH
            }
        }

        if (type instanceof ShortType) {
            try {
                newValue = virtualMachine.mirrorOf(Short.parseShort(
                    value.trim()));
            } catch (NumberFormatException e) {
                // SQUASH
            }
        }

        if (type instanceof ClassType) {
            if (type.name().equals("java.lang.String")) {
                newValue = virtualMachine.mirrorOf(value);
            }
        }

        if (newValue == null) {
            // We are unable to modiy.
            return;
        }

        try {
            if (localVariable != null) {
                frame.setValue(localVariable, newValue);
            }
            if (field != null) {
                if (objectReference != null) {
                    objectReference.setValue(field, newValue);
                }
                if (referenceType != null) {
                    if (referenceType instanceof ClassType) {
                        ((ClassType) referenceType).setValue(field, newValue);
                    }
                }
            }
            if (arrayValue != null) {
                arrayReference.setValue(arrayIndex, newValue);
            }
        } catch (ClassNotLoadedException e) {
            // SQUASH
        } catch (InvalidTypeException e) {
            // SQUASH
        } catch (IllegalArgumentException e) {
            // Tried to modify a final field.
            // SQUASH
        }
    }

    /**
     * Check if this value can be modified.
     *
     * @return true if this value can be modified
     */
    public boolean canModify() {
        Type type = null;
        try {
            if (localVariable != null) {
                type = localVariable.type();
            }
            if (field != null) {
                type = field.type();
            }
            if (arrayValue != null) {
                type = arrayValue.type();
            }
        } catch (ClassNotLoadedException e) {
            // Error, so we cannot modify.
            return false;
        }

        if ((type instanceof BooleanType)
            || (type instanceof ByteType)
            || (type instanceof CharType)
            || (type instanceof DoubleType)
            || (type instanceof FloatType)
            || (type instanceof IntegerType)
            || (type instanceof LongType)
            || (type instanceof ShortType)
        ) {
            // Primitive types can be set OK.
            return true;
        }

        if (type instanceof ClassType) {
            if (type.name().equals("java.lang.String")) {
                // Can modify String
                return true;
            }
        }

        // Don't know how to set these types.
        return false;
    }

    /**
     * Check if this a compound value (array or class).
     *
     * @return true if this is a compound value such as array or class
     */
    public boolean isCompound() {
        Type type = null;
        try {
            if (localVariable != null) {
                type = localVariable.type();
            }
            if (field != null) {
                type = field.type();
            }
            if (arrayValue != null) {
                type = arrayValue.type();
            }
        } catch (ClassNotLoadedException e) {
            // Error, so we do not know.
            return false;
        }

        if ((type instanceof BooleanType)
            || (type instanceof ByteType)
            || (type instanceof CharType)
            || (type instanceof DoubleType)
            || (type instanceof FloatType)
            || (type instanceof IntegerType)
            || (type instanceof LongType)
            || (type instanceof ShortType)
        ) {
            // Primitive types are not compound values.
            return false;
        }

        if (type instanceof ClassType) {
            if (type.name().equals("java.lang.String")) {
                // Strings are not compound values.
                return false;
            } else {
                // Other classes are compound values.
                return true;
            }
        }

        if (type instanceof ArrayType) {
            // Arrays are compound values.
            return true;
        }

        // Don't know about this type, assume it is terminal.
        return false;
    }

    /**
     * Get the sub-values of this value, if this is a compound type.
     *
     * @return the values, or null if this is not a compound type
     */
    public List<DebugValue> getSubValues() {
        Type type = null;
        try {
            if (localVariable != null) {
                type = localVariable.type();
            }
            if (field != null) {
                type = field.type();
            }
            if (arrayValue != null) {
                type = arrayValue.type();
            }
        } catch (ClassNotLoadedException e) {
            // Error, so we cannot obtain.
            return null;
        }

        if ((type instanceof BooleanType)
            || (type instanceof ByteType)
            || (type instanceof CharType)
            || (type instanceof DoubleType)
            || (type instanceof FloatType)
            || (type instanceof IntegerType)
            || (type instanceof LongType)
            || (type instanceof ShortType)
        ) {
            // Primitive types are not compound values.
            return null;
        }

        if (type instanceof ClassType) {
            if (type.name().equals("java.lang.String")) {
                // Strings are not compound values.
                return null;
            }
        } else if (!(type instanceof ArrayType)) {
            // This is neither class nor array, don't know what to do.
            return null;
        }

        assert ((type instanceof ClassType) || (type instanceof ArrayType));

        // We have filtered off the non-compound types, now build the list of
        // values to return.
        List<DebugValue> values = new ArrayList<DebugValue>();
        Value value = null;
        if (field != null) {
            if (objectReference != null) {
                try {
                    value = objectReference.getValue(field);
                } catch (IllegalArgumentException e) {
                    value = null;
                }
            }
            if (referenceType != null) {
                try {
                    value = referenceType.getValue(field);
                } catch (IllegalArgumentException e) {
                    value = null;
                }
            }
        }
        if (localVariable != null) {
            value = frame.getValue(localVariable);
        }
        if (arrayValue != null) {
            value = arrayValue;
        }
        if (value == null) {
            // Ideally shouldn't get here, but bail out anyway.
            return null;
        }

        if (value instanceof ArrayReference) {
            ArrayReference arr = (ArrayReference) value;
            int i = 0;
            for (Value v: arr.getValues()) {
                JavaDebugValue javaValue = new JavaDebugValue(i, v, arr);
                values.add(javaValue);
                i++;
            }
            return values;
        }

        if (value instanceof ObjectReference) {
            ObjectReference obj = (ObjectReference) value;
            for (Field f: ((ReferenceType) obj.type()).allFields()) {
                if (!f.isStatic()) {
                    JavaDebugValue javaValue = new JavaDebugValue(f, obj);
                    values.add(javaValue);
                }
            }
            return values;
        }

        // Really shouldn't get here, but bail out anyway.
        return null;
    }


}
